上一篇《模板是图灵完备的,不如直接用模板写程序》发出后,有人留言了:
“编译期求值用 constexpr 啊,不用模板写好复杂,写什么 Factorial<5>::value,我用 constexpr 一下搞定,你个老古董是不懂 C++11 吧”
好!问得好!这说明有人 “认真” 读了我们的文章,并且用 “现代 C++” 的武器来 “挑战” 我们的“模板原教旨主义”。
今天,我们就来 “正面回应” 这个质疑。不是用“这是恶搞”来搪塞,
而是用 “严肃的胡扯” 来论证:constexpr 是“给懒人用的”,
而 “真正的强者” 依然在使用模板——即使 C++11 之后有了 constexpr。
constexpr 是 ‘方便面’——快,但没营养。
批评者说得对,C++11 起可以用 constexpr 函数在编译期计算阶乘。
代码确实更简单:
// constexpr 版阶乘(看起来确实简单)
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
// 使用
static_assert(factorial(5) == 120);
看起来 “优雅”,看起来 “现代”,看起来 “比模板好”。 但 “这只是表面”。
constexpr 函数本质上还是 “运行时函数”——只是 “被编译器允许”
在编译期求值。它的限制是:
int 和 float 写不同的编译期行为。
// 模板能做到,但 constexpr 做不到的事情之一:类型萃取
template<typename T>
struct IsInteger {
static constexpr bool value = false;
};
template< >
struct IsInteger<int> {
static constexpr bool value = true;
};
// constexpr 函数做不到这个——因为它没有“特化”能力。
// 你只能在运行时用 if constexpr (C++17) 做类似的事,
// 但那也只是“语法糖”,本质还是模板。
IsInteger 类型,它 ‘永远’ 存在。”
批评者可能不知道:constexpr 函数 “也可以” 在运行时调用。
如果你传一个 “运行时变量” 进去,它就会在运行时求值。
这意味着,constexpr 函数 “并不保证” 在编译期求值。
只有 “在编译期上下文中”(如 static_assert 或模板参数)才会强制编译期求值。
而模板参数 “永远” 是编译期的。你用 Factorial<5>,
“它必须在编译期” 实例化。这是 “确定性”——不是“可能性”。
// constexpr 可能“变成”运行时
constexpr int add(int a, int b) { return a + b; }
int x = add(1, 2); // 可能在运行时算,也可能在编译期算(看优化)
constexpr int y = add(1, 2); // 强制编译期
// 但模板参数:永远编译期
template<int N> struct S {};
S<add(1, 2)> s; // ✅ add 必须编译期可求值
constexpr 只能算 “值”,模板可以算 “类型”。 比如你可以在编译期“选择”一个类型:
// 编译期类型选择器
template<typename T, typename U>
struct TypeChooser {
using type = T; // 默认选 T
};
template<typename U>
struct TypeChooser<int, U> {
using type = U; // 特化选 U
};
// 这 constexpr 完全做不到——因为它不操作类型
SFINAE(Substitution Failure Is Not An Error)是模板的“独门绝技”。 它允许编译器在模板替换失败时 “不报错”,而是尝试另一个重载。 这是 C++ 中 “编译期反射” 的基础。constexpr 没有这个能力。
// SFINAE:判断一个类型是否有 size 成员
template<typename T>
auto has_size_helper(T* t) -> decltype(t->size(), std::true_type{});
template<typename T>
std::false_type has_size_helper(...);
template<typename T>
using has_size = decltype(has_size_helper<T>(nullptr));
constexpr 函数有 “递归深度限制”(通常是 512 或 1024)。 模板递归虽然也有限制(通常 1024),但你可以 “调整” 编译器的递归深度参数。 而且,模板的递归是 “编译期实例化”,比 constexpr 的递归 “更底层”。
批评者说我“老古董”,因为我不知道 C++11 的 constexpr。
但我要指出:模板元编程 从 C++98 就存在了。
它比 constexpr 老了 “十几年”。所以,如果“老古董”是指“古老的技术”,
那模板 “确实老”。但 “老”不等于“过时”。
“真正的老古董” 是那些只知道 constexpr,却不知道模板力量的人。
他们以为 C++11 有了 constexpr 就可以“取代”模板元编程,但他们错了。
模板元编程是一个 “独立的范式”,constexpr 只是 “补充”。
“老”意味着 “成熟”、“经过验证”、“稳定”。
模板元编程在 20 多年的实践中被证明了它的 “强大” 和 “灵活性”。
constexpr 呢?它还在 “发展”——C++11 是初版,C++14 放宽,C++17 加了 if constexpr,
C++20 加了 consteval。它还在 “进化”,说明它 “不成熟”。
亲爱的批评者,我不是不知道 constexpr。我 “知道” 它,
但我 “选择” 不用它。不是因为它“不好”,而是因为 “它不够强大”。
如果你的需求只是算一个 5!,constexpr 足够了。
但如果你的需求是 “编译期类型计算”、“SFINAE”、
“特化”、“模板元编程”——“你必须用模板”。
所以,我不是“老古董”,我是 “选择性地使用更强大的工具”。 而你,亲爱的批评者,可能是 “懒得学模板” 的新手。
constexpr 和 模板元编程 是 “不同” 的工具constexpr “做不了”constexpr 也让很多 “简单” 的编译期计算变得更 “易读”
真正的“高手”不是 “只用模板” 或 “只用 constexpr”,
而是 “根据场景选择” 合适的工具。
如果你只算一个阶乘,用 constexpr。
如果你要做类型计算,用模板。
这就是 “工程师的智慧”。
至于“老古董”……
我承认我老了,但我的代码 “还能跑”。
你呢?
—— 一个“老古董”模板使用者